package com.atguigu.gmall.gateway.filter;


import com.alibaba.nacos.common.utils.StringUtils;
import com.atguigu.gmall.common.constant.RedisConst;
import com.atguigu.gmall.common.result.Result;
import com.atguigu.gmall.common.result.ResultCodeEnum;
import com.atguigu.gmall.common.util.Jsons;
import com.atguigu.gmall.gateway.properties.AuthUrlProperties;
import com.atguigu.gmall.user.entity.UserInfo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.http.HttpCookie;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseCookie;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.nio.charset.StandardCharsets;
import java.util.List;

/**
 * 网关的全局过滤器。每个请求过来过滤器都会启动拦截。和HttpFilter
 * <p>
 * 统一鉴权 + 用户id透传
 */
@Slf4j
@Component
public class UserAuthFilter implements GlobalFilter {

    /**
     * 1、响应式编程：
     * 所有操作都可以默认是异步的，不用等待。需要结果的时候，感知结果
     * 利用 【发布-订阅】 模式
     * jisuan();
     * write();  //开异步。      发布者，最终会把做好的结果发布出去
     * xxxx();
     * jieguo(); //等待上次结果   订阅者，感知上次做完的结果。
     * <p>
     * future.whenComplete()
     * <p>
     * 2、阻塞式编程：
     * jisuan();
     * write();
     * xxxx();
     * jieguo();
     * <p>
     * Mono。Flux。 包装的数据具有响应式特征；什么时候有数据，什么时候就发布出去，订阅者就能看到
     *
     * @param args
     */
//    public static void test01(String[] args) throws InterruptedException {
//
//        //发布者  Mono：发布一个或0个数据。 由jvm内部线程调度好，连异步都不用写但就是异步
////        Mono<String> just = Mono.just("哈哈");
////
////        //订阅者
////        just.subscribe((item)-> System.out.println("1:"+item));
////        just.subscribe((item)-> System.out.println("2:"+item));
////        just.subscribe((item)-> System.out.println("3:"+item));
//
//
//        Flux<Long> interval = Flux.interval(Duration.ofMillis(1000));
//
//
//        interval.subscribe((item) -> {
//            System.out.println(Thread.currentThread() + "1号：" + item);
//        });
//
//
//        interval.subscribe((item) -> {
//            try {
//                Thread.sleep(3000);
//            } catch (InterruptedException e) {
//                e.printStackTrace();
//            }
//            System.out.println(Thread.currentThread() + "2号：" + item);
//        });
//
////        System.out.println("haha:"+new Date());
////        //方法不阻塞
////        Mono<String> genstr = genstr();
////        System.out.println("hehe:"+new Date());
////
////
////        //感兴趣结果的人自己订阅即可
////        genstr.subscribe((item)->{
////            System.out.println("张三："+item);
////        });
////
////        genstr.subscribe((item)->{
////            System.out.println("李四："+item);
////        });
////
////        genstr.subscribe((item)->{
////            System.out.println("王五："+item);
////        });
//
//
//        Thread.sleep(10000000);
//
//    }
//
//    public static Mono<String> genstr() {
//
//        Mono<String> mono = Mono.fromCallable(() -> {
//            Thread.sleep(3000);
//            return UUID.randomUUID().toString();
//        });
//        return mono;
//    }


    @Autowired
    AuthUrlProperties urlProperties;

    AntPathMatcher matcher = new AntPathMatcher();

    @Autowired
    StringRedisTemplate redisTemplate;

    /**
     * @param exchange 请求和响应都在这里封装着
     * @param chain    filter链
     * @return
     */
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        //URL： URI
        //url(完整路径): https://docs.spring.io/spring-cloud-gateway/docs/2.2.9.RELEASE/reference/html/#global-filters
        //uri(忽略服务器域名): /spring-cloud-gateway/docs/2.2.9.RELEASE/reference/html/#global-filters
        String path = exchange.getRequest().getURI().getPath();
        log.info("请求开始：请求路径：{}", path);

        // /js/** /css/** /img/**
        //1、【无需认证】如果请求静态资源，直接放行
        List<String> noauthurl = urlProperties.getNoauthurl();
        for (String pattern : noauthurl) {
            // 判断当前请求是否满足这种路径格式
            if (matcher.match(pattern, path)) {
                return chain.filter(exchange);
            }
        }


        //2、【任何情况浏览器都不能访问的 /api/inner/**】
        for (String pattern : urlProperties.getDenyurl()) {
            if (matcher.match(pattern, path)) {
                //直接拒绝。响应一个json即可
                Result<String> result = Result.build("", ResultCodeEnum.PERMISSION);
                return responseJson(exchange, result);
            }
        }


        //3、【需要登录】如果请求路径是需要登录才能访问。验证登录
        for (String pattern : urlProperties.getLoginauth()) {
            if (matcher.match(pattern, path)) {
                //2.1、先拿到用户的登录后请求传来的 token
                String token = getUserToken(exchange);
                if (!StringUtils.isEmpty(token)) {
                    //2.2、 说明用户登录了
                    //2.2.1、【令牌真伪校验】是真令牌还是假令牌
                    UserInfo info = getUserInfoByToken(token);
                    if (info == null) {
                        //2.2.2、 假令牌，伪造登录
                        return locationToUrl(exchange, urlProperties.getLoginPage());
                    }
                    //2.2.3、真令牌直接放行。透传用户id【给request中加一个userid的请求头】再放行
                    return userIdThrought(chain, exchange, info);
                } else {
                    //2.3、 说明用户没登录。网关直接把请求打回到登录页
                    return locationToUrl(exchange, urlProperties.getLoginPage());
                }
            }
        }


        //========满足特定规则的请求==============

        //4、普通请求。【用户没带token 拉倒。如果带了，就透传id】
        String userToken = getUserToken(exchange);
        if (!StringUtils.isEmpty(userToken)) {
            //带了令牌。顺便验证下令牌对不对
            UserInfo userInfo = getUserInfoByToken(userToken);
            if (userInfo == null) {
                //令牌错误
                return locationToUrl(exchange, urlProperties.getLoginPage());
            } else {
                //令牌正确。
                return userIdThrought(chain, exchange, userInfo);
            }
        } else {
            //不带令牌的不用验证。也需要把临时id透传下去
            return userTempIdThrought(chain, exchange);
        }
//        return chain.filter(exchange);
    }

    /**
     * 普通请求没带 token，说明没登录。
     * 只要带了临时id，无论任何情况都要透传
     *
     * @param chain
     * @param exchange
     * @return
     */
    private Mono<Void> userTempIdThrought(GatewayFilterChain chain, ServerWebExchange exchange) {

        String tempId = getUserTempId(exchange);
        //1、准备一个新请求对象
        ServerHttpRequest newRequest = exchange.getRequest()
                .mutate()
                .header(RedisConst.USER_TEMP_ID_HEADER, tempId)
                .build();

        //2、准备一个新exchange对象
        ServerWebExchange webExchange = exchange.mutate()
                .request(newRequest)  //请求
                .response(exchange.getResponse()) //响应
                .build();

        return chain.filter(webExchange);
    }

    /**
     * 给浏览器写一个json
     *
     * @param exchange
     * @param result
     * @return
     */
    private Mono<Void> responseJson(ServerWebExchange exchange,
                                    Result<String> result) {
        String jsonStr = Jsons.toStr(result);

        //1、拿到响应写数据出去
        ServerHttpResponse response = exchange.getResponse();
        response.getHeaders().setContentType(MediaType.APPLICATION_JSON_UTF8);

        // 2、创建要写出去的数据
        DataBuffer dataBuffer = response.bufferFactory().wrap(jsonStr.getBytes(StandardCharsets.UTF_8));

        //3、写出去
        return response.writeWith(Mono.just(dataBuffer));
    }


    /**
     * 透传用户id。
     * 放行请求。
     *
     * @param chain
     * @param exchange 请求、响应
     * @param info
     * @return
     */
    private Mono<Void> userIdThrought(GatewayFilterChain chain,
                                      ServerWebExchange exchange,
                                      UserInfo info) {
        Long userId = info.getId();

        //放行之前给请求头中加一个用户id
        //默认请求头是只读的
//        exchange.getRequest()
//                .getHeaders()
//                .add(RedisConst.USER_ID_HEADER,userId+"");
        //响应式编程中怎么修改请求头
        String tempId = getUserTempId(exchange);
        //1、准备一个新请求对象
        ServerHttpRequest newRequest = exchange.getRequest()
                .mutate()
                .header(RedisConst.USER_ID_HEADER, userId.toString())
                .header(RedisConst.USER_TEMP_ID_HEADER, tempId)
                .build();

        //2、准备一个新exchange对象
        ServerWebExchange webExchange = exchange.mutate()
                .request(newRequest)  //请求
                .response(exchange.getResponse()) //响应
                .build();

        return chain.filter(webExchange);
    }

    private String getUserTempId(ServerWebExchange exchange) {
        ServerHttpRequest request = exchange.getRequest();
        String tempId = "";
        //1、尝试从请求头中 cookie字段，获取值
        HttpCookie cookie = request.getCookies().getFirst("userTempId");
        if (cookie != null) {
            tempId = cookie.getValue();
        } else {
            //2、尝试从请求头中 token 字段
            tempId = request.getHeaders().getFirst("userTempId");
        }
        return tempId;
    }

    /**
     * 按照用户传来的令牌，去redis查找用户完整信息
     *
     * @param token
     * @return
     */
    private UserInfo getUserInfoByToken(String token) {
        String json = redisTemplate.opsForValue().get(RedisConst.USER_LOGIN + token);
        if (!StringUtils.isEmpty(json)) {
            UserInfo info = Jsons.toObj(json, UserInfo.class);
            return info;
        }
        return null;
    }


    /**
     * 跳转到指定页面
     *
     * @param exchange 请求、响应对象
     * @param page     要跳转的页面
     * @return
     */
    private Mono<Void> locationToUrl(ServerWebExchange exchange, String page) {
        //重定向到指定页
        ServerHttpResponse response = exchange.getResponse();
        String path = exchange.getRequest().getURI().toString();
        //重定向原理：
        // 响应头：    Location: http://www.baidu.com
        // 响应状态码： 302
        response.setStatusCode(HttpStatus.FOUND);
        //
        response.getHeaders().add("Location", page + "?originUrl=" + path);

        //命令浏览器清空假的cookie；服务器只需要发一个同名cookie，maxAge=0
        ResponseCookie cookie = ResponseCookie.from("token", "1")
                .domain(".gmall.com")
                .maxAge(0)
                .build();
        response.addCookie(cookie);

        return response.setComplete();
    }


    /**
     * 获取用户令牌
     *
     * @param exchange
     * @return
     */
    private String getUserToken(ServerWebExchange exchange) {
        ServerHttpRequest request = exchange.getRequest();
        String userToken = "";
        //1、尝试从请求头中 cookie字段，获取值
        HttpCookie cookie = request.getCookies().getFirst("token");
        if (cookie != null) {
            userToken = cookie.getValue();
        } else {
            //2、尝试从请求头中 token 字段
            userToken = request.getHeaders().getFirst("token");
        }

        return userToken;
    }
}
